import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Date;

//Java泛型的实现方法：类型擦除
//大家都知道，Java的泛型是伪泛型，这是因为Java在编译期间，所有的泛型信息都会被擦掉，正确理解泛型概念的首要前提是理解类型擦除。
// Java的泛型基本上都是在编译器这个层次上实现的，在生成的字节码中是不包含泛型中的类型信息的，使用泛型的时候加上类型参数，
// 在编译器编译的时候会去掉，这个过程成为类型擦除。
//
//如在代码中定义List<Object>和List<String>等类型，在编译后都会变成List，JVM看到的只是List，而由泛型附加的类型信息对JVM是看不到的。
// Java编译器会在编译时尽可能的发现可能出错的地方，但是仍然无法在运行时刻出现的类型转换异常的情况，
// 类型擦除也是Java的泛型与C++模板机制实现方式之间的重要区别。
//https://blog.csdn.net/xmtblog/article/details/103966859
public class GenericsDemo {

    //原始类型相等
    private void test() {
        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);
        System.out.println(list1.getClass() == list2.getClass());

        //在这个例子中，我们定义了两个ArrayList数组，不过一个是ArrayList<String>泛型类型的，
        //只能存储字符串；一个是ArrayList<Integer>泛型类型的，只能存储整数，
        //最后，我们通过list1对象和list2对象的getClass()方法获取他们的类的信息，最后发现结果为true。
        //说明泛型类型String和Integer都被擦除掉了，只剩下原始类型。
    }

    //通过反射添加其它类型元素
    private void test2() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);  //这样调用 add 方法只能存储整形，因为泛型类型的实例为 Integer
        list.getClass().getMethod("add", Object.class).invoke(list, "asd");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(list.get(i));
        }

        //在程序中定义了一个ArrayList泛型类型实例化为Integer对象，如果直接调用add()方法，那么只能存储整数数据，
        //不过当我们利用反射调用add()方法的时候，却可以存储字符串，这说明了Integer泛型实例在编译之后被擦除掉了，只保留了原始类型。
    }

    //原始类型Object
    private void test3() {
        //原始类型 就是擦除去了泛型信息，最后在字节码中的类型变量的真正类型，无论何时定义一个泛型，
        //相应的原始类型都会被自动提供，类型变量擦除，并使用其限定类型（无限定的变量用Object）替换。
        class Pair<T> {
            private T value;

            public T getValue() {
                return value;
            }

            public void setValue(T value) {
                this.value = value;
            }
        }

        // -> 原始类型
        class Pair2 {
            private Object value;

            public Object getValue() {
                return value;
            }

            public void setValue(Object value) {
                this.value = value;
            }
        }

        //因为在Pair<T>中，T 是一个无限定的类型变量，所以用Object替换，其结果就是一个普通的类，
        //如同泛型加入Java语言之前的已经实现的样子。在程序中可以包含不同类型的Pair，如Pair<String>或Pair<Integer>，
        //但是擦除类型后他们的就成为原始的Pair类型了，原始类型都是Object。
    }

    //这是一个简单的泛型方法
    public static <T> T add(T x, T y) {
        return y;
    }

    private void test4() {
        //要区分原始类型和泛型变量的类型。
        //在调用泛型方法时，可以指定泛型，也可以不指定泛型。
        //在不指定泛型的情况下，泛型变量的类型为该方法中的几种类型的同一父类的最小级，直到Object
        //在指定泛型的情况下，该方法的几种类型必须是该泛型的实例的类型或者其子类

        /**不指定泛型的时候*/
        int i = GenericsDemo.add(1, 2); //这两个参数都是Integer，所以T为Integer类型
        Number f = GenericsDemo.add(1, 1.2); //这两个参数一个是Integer，以风格是Float，所以取同一父类的最小级，为Number
        Object o = GenericsDemo.add(1, "asd"); //这两个参数一个是Integer，以风格是Float，所以取同一父类的最小级，为Object

        /**指定泛型的时候*/
        int a = GenericsDemo.<Integer>add(1, 2); //指定了Integer，所以只能为Integer类型或者其子类
        //int b = GenericsDemo.<Integer>add(1, 2.2); //编译错误，指定了Integer，不能为Float
        Number c = GenericsDemo.<Number>add(1, 2.2); //指定为Number，所以可以为Integer和Float

        //其实在泛型类中，不指定泛型的时候，也差不多，只不过这个时候的泛型为Object，就比如ArrayList中，如果不指定泛型，那么这个ArrayList可以存储任意的对象。
    }

    //Object泛型
    private void test5() {
        ArrayList list = new ArrayList();
        list.add(1);
        list.add("121");
        list.add(new Date());
    }

    //先检查，再编译以及编译的对象和引用传递问题
    private void test6() {
        //Q: 既然说类型变量会在编译的时候擦除掉，那为什么我们往 ArrayList 创建的对象中添加整数会报错呢？
        //不是说泛型变量String会在编译的时候变为Object类型吗？
        //为什么不能存别的类型呢？既然类型擦除了，如何保证我们只能使用泛型变量限定的类型呢？
        //A: Java编译器是通过先检查代码中泛型的类型，然后在进行类型擦除，再进行编译。

        ArrayList<String> list = new ArrayList<String>();
        list.add("123");
//        list.add(123);//编译错误
        //在上面的程序中，使用add方法添加一个整型，在IDE中，直接会报错，说明这就是在编译之前的检查，
        //因为如果是在编译之后检查，类型擦除后，原始类型为Object，是应该允许任意引用类型添加的。
        //可实际上却不是这样的，这恰恰说明了关于泛型变量的使用，是会在编译之前检查的。
    }

    private void test7() {
        //因为类型检查就是编译时完成的，new ArrayList()只是在内存中开辟了一个存储空间，
        //可以存储任何类型对象，而真正设计类型检查的是它的引用，因为我们是使用它引用list1来调用它的方法，
        //比如说调用add方法，所以list1引用能完成泛型类型的检查。而引用list2没有使用泛型，所以不行。
        ArrayList<String> list1 = new ArrayList();
        list1.add("1"); //编译通过
//        list1.add(1); //编译错误
        String str1 = list1.get(0); //返回类型就是String

        ArrayList list2 = new ArrayList<String>();
        list2.add("1"); //编译通过
        list2.add(1); //编译通过
        Object object = list2.get(0); //返回类型就是Object

        new ArrayList<String>().add("11"); //编译通过
//        new ArrayList<String>().add(22); //编译错误

        String str2 = new ArrayList<String>().get(0); //返回类型就是String

        //过上面的例子，我们可以明白，类型检查就是针对引用的，谁是一个引用，用这个引用调用泛型方法，就会对这个引用调用的方法进行类型检测，而无关它真正引用的对象。
        //泛型中参数话类型为什么不考虑继承关系？
        //在Java中，像下面形式的引用传递是不允许的:
    }

    //自动类型转换
    private void test8() {
        //因为类型擦除的问题，所以所有的泛型类型变量最后都会被替换为原始类型。
        //既然都被替换为原始类型，那么为什么我们在获取的时候，不需要进行强制类型转换呢？

        //看下ArrayList.get()方法：
//        public E get(int index) {
//            RangeCheck(index);
//            return (E) elementData[index];
//        }

        //可以看到，在return之前，会根据泛型变量进行强转。假设泛型类型变量为Date，虽然泛型信息会被擦除掉，
        //但是会将(E) elementData[index]，编译为(Date)elementData[index]。
        //所以我们不用自己进行强转。当存取一个泛型域时也会自动插入强制类型转换。
    }

    //类型擦除与多态的冲突和解决方法
    private void test9() {
        //class Pair<T> {
        //    private T value;
        //    public T getValue() {
        //        return value;
        //    }
        //    public void setValue(T value) {
        //        this.value = value;
        //    }
        //}

        //类型擦除后
        class Pair {
            private Object value;

            public Object getValue() {
                return value;
            }

            public void setValue(Object value) {
                this.value = value;
            }
        }

        //重裁 or 多态
        //可是由于种种原因，虚拟机并不能将泛型类型变为Date，只能将类型擦除掉，变为原始类型Object。
        //这样，我们的本意是进行重写，实现多态。可是类型擦除后，只能变为了重载。
        //这样，类型擦除就和多态有了冲突。JVM知道你的本意吗？知道！！！可是它能直接实现吗，不能！！！
        //如果真的不能的话，那我们怎么去重写我们想要的Date类型参数的方法啊。
        //于是JVM采用了一个特殊的方法，来完成这项功能，那就是桥方法。

        //从编译的结果来看，我们本意重写setValue和getValue方法的子类，竟然有4个方法，其实不用惊奇，
        //最后的两个方法，就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object，
        //也就是说，子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。
        //而桥方法的内部实现，就只是去调用我们自己重写的那两个方法。
        //所以，虚拟机巧妙的使用了桥方法，来解决了类型擦除和多态的冲突。
    }

    //泛型类型变量不能是基本数据类型
    private void test10() {
        //不能用类型参数替换基本类型。就比如，没有ArrayList<double>，只有ArrayList<Double>。
        //因为当类型擦除后，ArrayList的原始类型变为Object，但是Object类型不能存储double值，只能引用Double的值。
    }

    //运行时类型查询
    private void test11() {
        ArrayList<String> arrayList = new ArrayList<String>();
        //因为类型擦除之后，ArrayList只剩下原始类型，泛型信息String不存在了。
        //那么，运行时进行类型查询的时候使用下面的方法是错误的
//        if (arrayList instanceof ArrayList<String>) {
//        }
    }

    //泛型在静态方法和静态类中的问题
    private void test12() {
        //泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数
//        public class Test2<T> {
//            public static T one;   //编译错误
//            public static  T show(T one){ //编译错误
//                return null;
//            }
//        }

        //因为泛型类中的泛型参数的实例化是在定义对象的时候指定的，而静态变量和静态方法不需要使用对象来调用。
        //对象都没有创建，如何确定这个泛型参数是何种类型，所以当然是错误的。

        //但是要注意区分下面的一种情况：
//        public static <T >T show(T one){ //这是正确的
//            return null;
//        }
        //因为这是一个泛型方法，在泛型方法中使用的T是自己在方法中定义的 T，而不是泛型类中的T。
    }

    public static void main(String[] args) {
    }
}
